from system.action import Action
from system.object import Object
from system.fileServer import fileServer
from system.networkServer import networkServer
from system import util

import time
import math
import stackless
import pygame

from OpenGL import GL
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

class Renderer(Action):
    def run(self, **args):
        self.res = args.get("res",(800,600))
        self.object.setVar("res",self.res)
        self.fullScreen = args.get("fullScreen",False)
        self.fps = args.get("fps",60)
        self.windowTitle = args.get("windowTitle","PASE")
        self.clearColor = args.get("clearColor",(0,0,1))
        self.lineWidth = args.get("lineWidth",2)
        self.ambientLight = args.get("ambientLight",(0.8,0.8,0.8))
        self.doLighting = args.get("doLighting",True)
        self.pickSize = args.get("pickSize",5)
        self.trackMouseMotion = args.get("trackMouseMotion",False)
        self.fov = args.get("fov",45.0)
        self.nearPlane = args.get("nearPlane",1.0)
        self.farPlane = args.get("farPlane",1000.0)
        
        self.initGfx()
        self.initInput()
        self.runGfx()
        
    def initInput(self):
        self.initKeys()
        self.initJoysticks()
        
        self.inputFunctions = util.getFunctions(self,"input_")
        
        self.mouseButtonNames = {1:'left',2:'middle',3:'right'}
        self.inputTasklet = stackless.tasklet(self.inputPump)()
        self.tasklets.append(self.inputTasklet)
        
    def initKeys(self):
        self.keys = {}
        for name in dir(pygame):
            if name.startswith('K_'):
                self.keys[getattr(pygame,name)] = name[2:].lower()
                
        self.keymods = {}
        for name in dir(pygame):
            if name.startswith("KMOD_"):
                self.keymods[name[5:].lower()] = getattr(pygame,name)
                
    def initJoysticks(self):
        self.joystickCount = pygame.joystick.get_count()
        if self.joystickCount:
            self.log("%d joysticks found" % self.joystickCount)
            
        self.joysticks = []
        for i in range(self.joystickCount):
            joystick = pygame.joystick.Joystick(i)
            joystick.init()
            self.joysticks.append(joystick)
            
    def inputPump(self):
        while True:
            self.schedule()
            
            try:
                events = pygame.event.get()
            except:
                self.log("pygame shut down, killing input tasklet")
                break
            
            for event in events:
                eventName = pygame.event.event_name(event.type)
                eventName = eventName[0].lower() + eventName[1:]
                if eventName in self.inputFunctions:
                    self.inputFunctions[eventName](event)
                    
    def input_quit(self, evt):
        self.log("pygame quit")
        #self.killAll()
        networkServer.killNetwork()
        
    def input_mouseButtonDown(self, event):
        buttonName = self.mouseInfo(event)
        self.publish("mouseButtonDown",button = event.button, pos = event.pos, relPos = self.relPos(event.pos), buttonName = buttonName)
        
        self.doPicking(event.pos, event.button, "Down")
        
    def input_mouseButtonUp(self, event):
        buttonName = self.mouseInfo(event)
        self.publish("mouseButtonUp",button = event.button, pos = event.pos, relPos = self.relPos(event.pos), buttonName = buttonName)
        
        self.doPicking(event.pos, event.button, "Up")
        
    def input_mouseMotion(self, event):
        if self.trackMouseMotion:
            self.publish("mouseMotion", pos = event.pos, relPos = self.relPos(event.pos), buttons = event.buttons)
     
    def input_keyDown(self, event):
        name, mods = self.keyInfo(event)
        self.publish("keyDown",name = name, mods = mods, key = event.key)
        
    def input_keyUp(self, event):
        name, mods = self.keyInfo(event)
        self.publish("keyUp",name = name, mods = mods, key = event.key)
        
    def input_joyButtonDown(self, event):
        self.publish("joyButtonDown",joy = event.joy, button = event.button)
        
    def input_joyButtonUp(self, event):
        self.publish("joyButtonup", joy = event.joy, button = event.button)
        
    def input_joyAxisMotion(self, event):
        self.publish("joyAxisMotion", joy = event.joy, value = event.value, axis = event.axis)
    
    def doPicking(self, pos, button, suffix):
        from OpenGL import GL
        if hasattr(GL,"glSelectWithCallback"):
            selectFunction = GL.glSelectWithCallback
        else:
            selectFunction = my_glSelectWithCallback
        
        self.pickMap = {}
        self.currentName = 1
        
        def pickRender2d():
            self.set2dProjection(True)
            self.render2d(True)
            
        def pickRender3d():
            self.set3dProjection(True)
            self.render3d(True)
        
        buf = list(selectFunction(pos[0],pos[1],pickRender2d,self.pickSize,self.pickSize))
        buf.extend(list(selectFunction(pos[0],pos[1],pickRender3d,self.pickSize,self.pickSize)))
        
        hits = [i[2][0] for i in buf]
        if hits:
            for hit in hits:
                self.publish("objectClick" + suffix,button = button, pos = pos, \
                    relPos = self.relPos(pos), object = self.pickMap[hit])
                    
    def mouseInfo(self, event):
        if event.button in self.mouseButtonNames:
            return self.mouseButtonNames[event.button]
        else:
            return None

    def keyInfo(self, event):
        if event.key in self.keys:
            name = self.keys[event.key]
        else:
            name = None
            
        mods = []
        for modName, mod in self.keymods.iteritems():
            if mod & event.mod:
                mods.append(modName)
                
        return name, mods
    
    def relPos(self, pos):
        return (pos[0]/float(self.res[0]),pos[1]/float(self.res[1]))
        
    def initGfx(self):
        pygame.init()
        glutInit(sys.argv)
        
        flags = pygame.OPENGL | pygame.DOUBLEBUF | pygame.HWSURFACE
        if self.fullScreen:
            flags |= pygame.FULLSCREEN
            
        self.screen = pygame.display.set_mode(self.res,flags)
        pygame.display.set_caption(self.windowTitle)
        
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_TEXTURE_2D)
        glShadeModel(GL_SMOOTH)
        if self.doLighting:
            glEnable(GL_LIGHTING)
            glLightModelfv(GL_LIGHT_MODEL_AMBIENT,self.ambientLight)
            
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA)
        glLineWidth(self.lineWidth)
        glClearColor(self.clearColor[0],self.clearColor[1],self.clearColor[2],1)
        
        self.lists = {}
        self.textures = {}
        self.failedTextures = []
        self.maxLights = 8
        self.recordingName = self.recordingList = None
        
        self.render2dFunctions = util.getFunctions(self,"render2d_")
        self.render3dFunctions = util.getFunctions(self,"render3d_")
        
    def runGfx(self):
        frameTime = 1.0 / float(self.fps)
        
        fpsTime = 5
        fpsStart = time.time()
        numFrames = 0
        
        for atime, dtime in self.enumerateTime(frameTime):
            numFrames += 1
            currentTime = time.time()
            if currentTime - fpsStart > fpsTime:
                self.log(float(numFrames) / (currentTime - fpsStart))
                fpsStart = currentTime
                numFrames = 0
        
            self.render()
            
    def render(self, picking = False):
        self.preRender()
        self.set2dProjection(picking)
        self.render3d(picking)
        self.render2d(picking)
        self.postRender()
        
    def preRender(self):
        self.rendered = []
        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT)
        
    def postRender(self):
        glFlush()
        pygame.display.flip()
        
    def set2dProjection(self, picking):
        if not picking:
            glMatrixMode(GL_PROJECTION)
            glLoadIdentity()
            gluOrtho2D(0,self.res[0],0,self.res[1])
            
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        glTranslate(0,self.res[1],0)
        glScalef(1,-1,1)
        
        if self.doLighting:
            glDisable(GL_LIGHTING)
            
        glColor(1,1,1,1)
        self.unbindTexture()
        
    def set3dProjection(self, picking):
        if not picking:
            glMatrixMode(GL_PROJECTION)
            glLoadIdentity()
            gluPerspective(self.fov,float(self.res[0])/float(self.res[1]),self.nearPlane,self.farPlane)
            
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        
        if self.doLighting:
            glEnable(GL_LIGHTING)
            
        glColor(1,1,1,1)
        self.unbindTexture()
        
    #FIXME: traverse scene graph from root down
    def render2d(self, picking):
        for obj in Object._objects.values():
            self.doRender2d(obj,picking)
            
    def doRender2d(self, obj, picking):
        if obj.name not in self.rendered and obj.hasVar("render2d") and obj.getVar("shown",True):
            self.rendered.append(obj.name)
            
            doMatrix = obj.hasVar("pos") or obj.hasVar("scale") or obj.hasVar("rot")
            if doMatrix:
                glPushMatrix()
                
                if obj.hasVar("pos"):
                    x,y = obj.getVar("pos",(0,0))
                    if obj.getVar("absolute",False):
                        glTranslatef(x,y,0)
                    else:
                        glTranslatef(x*float(self.res[0]),y*float(self.res[1]),0)
                        
                if obj.hasVar("rot"):
                    glRotatef(math.degrees(obj.getVar("rot",0)),0,0,1)
                    
                if obj.hasVar("scale"):
                    x,y = obj.getVar("scale",(1,1))
                    if obj.getVar("absolute",False):
                        glScalef(x,y,1)
                    else:
                        glScalef(x*float(self.res[0]),y*float(self.res[1]),1)
                        
            if picking:
                self.pickMap[self.currentName] = obj.name
                glPushName(self.currentName)
                self.currentName += 1
                
            elif obj.getVar("static",True):
                if obj.name in self.lists:
                    glCallList(self.lists[obj.name])
                    if doMatrix:
                        glPopMatrix()
                    return
                elif not self.recordingName and not self.recordingList:
                    self.recordingName = obj.name
                    self.recordingList = glGenLists(1)
                    glNewList(self.recordingList,GL_COMPILE_AND_EXECUTE)
                
            doAttrib = obj.hasVar("color") or obj.hasVar("texture")
            
            if doAttrib:
                glPushAttrib(GL_ENABLE_BIT | GL_CURRENT_BIT)
                
                if obj.hasVar("color"):
                    glColor(*obj.getVar("color",(1,1,1,1)))
                    
                if obj.hasVar("texture") and not picking:
                    self.bindTexture(obj.getVar("texture",None),obj.getVar("blendTexture",True))
                    
            renderOp = obj.getVar("render2d",None)
            assert renderOp in self.render2dFunctions, "unsupported 2d render op %s" % renderOp
            self.render2dFunctions[renderOp](obj)
            
            if obj.hasVar("children"):
                for childName in obj.getVar("children",[]):
                    self.doRender2d(Object.getObject(childName),picking)
                    
            if doAttrib:
                glPopAttrib(GL_ENABLE_BIT | GL_CURRENT_BIT)
                
            if picking:
                glPopName()
                
            if self.recordingName == obj.name:
                glEndList()
                self.lists[self.recordingName] = self.recordingList
                self.recordingName = self.recordingList = None
                
            if doMatrix:
                glPopMatrix()
                
    def render2d_node(self, obj):
        pass
    
    def render2d_custom(self, obj):
        if obj.hasVar("renderOp") and obj.hasVar("vertexList"):
            renderOp = getattr(GL,"GL_%s" % obj.getVar("renderOp",None).upper())
            vertexList = obj.getVar("vertexList",[])
            
            if obj.hasVar("texture"):
                if obj.hasVar("texCoordList"):
                    texCoordList = obj.getVar("texCoordList")
                else:
                    xlist = [v[0] for v in vertexList]
                    ylist = [v[1] for v in vertexList]
                    minx = min(xlist)
                    miny = min(ylist)
                    maxx = max(xlist)
                    maxy = max(ylist)
                    texCoordList = [((x-minx)/(maxx-minx),(y-miny)/(maxy-miny)) for x,y in vertexList]
                    obj.setVar("texCoordList",texCoordList)
                    
                glBegin(renderOp)
                for i, ((x,y),(tx,ty)) in enumerate(zip(vertexList,texCoordList)):
                    if obj.hasVar("colorList"):
                        glColor(*obj.getVar("colorList",None)[i])
                    glTexCoord2f(tx,ty)
                    glVertex2f(x,y)
                glEnd()
                
            else:
                glBegin(RenderOp)
                for i, (x,y) in enumerate(vertexList):
                    if obj.hasVar("colorList"):
                        glColor(*obj.getVar("colorList",None)[i])
                    glVertex2f(x,y)
                glEnd()
                
    def render2d_circle(self, obj):
        slices = obj.getVar("slices",20)
        if obj.getVar("filled",True):
            glBegin(GL_TRIANGLE_FAN)
            glTexCoord2f(0.5,0.5)
            glVertex2f(0,0)
        else:
            glBegin(GL_LINE_LOOP)
        for i in range(slices+1):
            th = float(i) / float(slices) * math.pi * 2
            x = math.cos(th)
            y = math.sin(th)
            glTexCoord2f((x+1)/2.0,(y+1)/2.0)
            glVertex2f(x,y)
        glEnd()
        
    def render2d_rect(self, obj):
        if obj.getVar("filled",True):
            glBegin(GL_QUADS)
        else:
            glBegin(GL_LINE_LOOP)
        glTexCoord2f(0,0)
        glVertex2f(0,0)
        glTexCoord2f(1,0)
        glVertex2f(1,0)
        glTexCoord2f(1,1)
        glVertex2f(1,1)
        glTexCoord2f(0,1)
        glVertex2f(0,1)
        glEnd()
        
    def render2d_tri(self, obj):
        if obj.getVar("filled",True):
            glBegin(GL_TRIANGLES)
        else:
            glBegin(GL_LINE_LOOP)
        glTexCoord2f(0,0)
        glVertex2f(0,-.5)
        glTexCoord2f(1,0.5)
        glVertex2f(1,0)
        glTexCoord2f(0,1)
        glVertex2f(0,.5)
        glEnd()
        
    def render2d_text(self, obj):
        s = obj.getVar("text",None)
        if s:
            if obj.getVar("center",False):
                width = sum([glutBitmapWidth(GLUT_BITMAP_HELVETICA_18,ord(ch)) for ch in s])
                glRasterPos(-width/2.0,0,0)
            else:
                glRasterPos(0,0,0)
                
            for ch in s:
                glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18,ord(ch))

    
    def render3d(self, picking):
        pass
    
    def loadTexture(self, filename, transparent):
        if filename not in fileServer.filePaths:
            return None
        
        filepath = fileServer.filePaths[filename]
        texture = pygame.image.load(filepath)
        
        w,h = texture.get_size()
        rgbTextureData = pygame.image.tostring(texture,"RGB",0)
        textureData = ""
        for i in range(0,len(rgbTextureData),3):
            r,g,b = [ord(j) for j in rgbTextureData[i:i+3]]
            if transparent and not r and not g and not b:
                a = 0
            else:
                a = 255
            textureData += chr(r) + chr(g) + chr(b) + chr(a)
        textureId = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D,textureId)
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST)
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT)
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT)
        gluBuild2DMipmaps(GL_TEXTURE_2D,GL_RGBA,w,h,GL_RGBA,GL_UNSIGNED_BYTE,textureData)
        glBindTexture(GL_TEXTURE_2D,0)
        return textureId
            
    def bindTexture(self, filename, transparent = False):
        if filename in self.failedTextures:
            return False
        
        if (filename, transparent) not in self.textures:
            texture = self.loadTexture(filename,transparent)
            if texture == None:
                self.failedTextures.append(filename)
                return False
            self.textures[(filename,transparent)] = texture
            
        glBindTexture(GL_TEXTURE_2D,self.textures[(filename,transparent)])
        return True
        
    def unbindTexture(self):
        glBindTexture(GL_TEXTURE_2D,0)

        
def my_glSelectWithCallback(x, y, callback, xsize = 5, ysize = 5, buffer_size = 512):
    viewport = glGetIntegerv(GL_VIEWPORT)
    glSelectBuffer(buffer_size)
    glRenderMode(GL_SELECT)
    glInitNames()
    glMatrixMode(GL_PROJECTION)
    previousviewmatrix = glGetDoublev(GL_PROJECTION_MATRIX)
    glLoadIdentity()
    gluPickMatrix(x, viewport[3] - y, xsize, ysize, viewport)
    glMultMatrixd(previousviewmatrix)
    callback()
    glFlush()
    glMatrixMode(GL_PROJECTION)
    glLoadMatrixd(previousviewmatrix)
    return glRenderMode(GL_RENDER)
